package com.zoe.onelink.auth.component;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.zoe.onelink.auth.serializer.FastjsonRedisTokenStoreSerializationStrategy;
import com.zoe.onelink.redis.util.RedisUtil;
import com.zoe.onelink.upms.common.entity.AuthInfo;
import com.zoe.onelink.upms.common.entity.UserDetailsInfo;
import com.zoe.onelink.upms.common.service.OneLinkTokenStore;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStoreSerializationStrategy;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 * 重写TokenStore，为资源服务器的TokenStore做定制化优化<br/>
 * 1. 去除RefreshToken和ZSET缓存（资源服务器不需要处理RefreshToken相关逻辑），由原来8个key缩小为2个key<br/>
 * 2. 重构OAuth2Authentication，减小redis内存开销<br/>
 * 原因：OAuth2Authentication在JSON序列化时，因getPrincipal()方法，导致多存了一份用户信息<br/>
 * 一份为OAuth2Authentication.principal，另一份为OAuth2Authentication.userAuthentication.principal<br/>
 * 用户中会保存权限信息，菜单越多，这个权限集合就会越大，存两份太浪费内存空间，这里重构成AuthInfo<br/>
 *
 * 3). 将权限信息和用户信息拆分开，权限标识列表单独存一份，AuthInfo中只存储用户信息和token信息<br/>
 *
 * 优化后key为：
 *
 * ONELINK:UPMS:TOKEN:AUTH:${access_token}              ->      AuthInfo
 * ONELINK:UPMS:TOKEN:USERNAME_2_ACCESS:${username}     ->      OAuth2AccessToken
 *
 * ONELINK:UPMS:TOKEN:PERMISSION:${access_token}        ->      List<String>
 *
 * ONELINK:UPMS:TOKEN:REFRESH:{refresh_token}           ->      OAuth2RefreshToken
 * ONELINK:UPMS:TOKEN:REFRESH_2_AUTH:{refresh_token}    ->      AuthInfo
 *
 * ONELINK:UPMS:TOKEN:ZSET:${clientId}                  ->      value=${access_token}，score=为到期时间戳，无期限为-1
 * ONELINK:UPMS:TOKEN:ZSET:${orgCode}                   ->      value=${access_token}，score=为到期时间戳，无期限为-1
 * ONELINK:UPMS:TOKEN:ZSET:${districtCode}              ->      value=${access_token}，score为到期时间戳，无期限为-1
 *
 * </p>
 * <p>描述: </p>
 * <p>版权: Copyright (c) 2018</p>
 * <p>公司: 智业软件</p>
 *
 * @author LinHongKai
 * @date 2022-02-24 15:13
 */
@RequiredArgsConstructor
public class OneLinkRedisTokenStore implements OneLinkTokenStore {

    private final static String ONELINK_UPMS_TOKEN = "ONELINK:UPMS:TOKEN:";

    private final static String AUTH = "AUTH:";

    private final static String USERNAME_2_ACCESS = "USERNAME_2_AUTH:";

    private final static String PERMISSION = "PERMISSION:";

    private final static String ZSET = "ZSET:";

    private final static String REFRESH = "REFRESH:";

    private final static String REFRESH_2_AUTH = "REFRESH_2_AUTH:";

    private RedisTokenStoreSerializationStrategy serializationStrategy = new FastjsonRedisTokenStoreSerializationStrategy();

    private final RedisTemplate<String, Object> redisTemplate;


    @Override
    public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
        // 资源服务器无需实现
    }


    @Override
    public void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication) {
        // 资源服务器无需实现
    }


    @Override
    public void flushAuthInfo(AuthInfo authInfo) {
        OAuth2AccessToken accessToken = authInfo.getAccessToken();
        OAuth2Authentication authentication = authInfo.buildAuthentication();
        String authInfoValue = serialize(authInfo);
        String oAuth2AccessTokenValue = serialize(accessToken);

        String authKey = getAuthKey(accessToken.getValue());
        String username2AccessKey = getUsername2AccessKey(authentication.getName());
        if (accessToken.getExpiration() != null) {
            int seconds = accessToken.getExpiresIn();
            RedisUtil.set(authKey, authInfoValue, seconds, TimeUnit.SECONDS);
            RedisUtil.set(username2AccessKey, oAuth2AccessTokenValue, seconds, TimeUnit.SECONDS);
        }
        else {
            RedisUtil.set(authKey, authInfoValue);
            RedisUtil.set(username2AccessKey, oAuth2AccessTokenValue);
        }
    }

    @Override
    public void flushPermission(OAuth2AccessToken token, List<String> permissions) {
        String permissionKey = getPermissionKey(token.getValue());
        // 有过期时间
        if (token.getExpiration() != null) {
            int seconds = token.getExpiresIn();
            if (CollectionUtil.isNotEmpty(permissions)) {
                RedisUtil.set(permissionKey, permissions, seconds, TimeUnit.SECONDS);
            } else {
                RedisUtil.deleteOne(permissionKey);
            }
        }
        // 永久
        else {
            if (CollectionUtil.isNotEmpty(permissions)) {
                RedisUtil.set(permissionKey, permissions);
            } else {
                RedisUtil.deleteOne(permissionKey);
            }
        }
    }


    @Override
    public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
        if(authentication == null) {
            return null;
        }
        return readAccessTokenByUsername(authentication.getName());
    }


    @Override
    public OAuth2AccessToken readAccessToken(String tokenValue) {
        String authInfoKey = getAuthKey(tokenValue);
        String authInfoValue = (String) RedisUtil.getObject(authInfoKey);
        AuthInfo authInfo = deserialize(authInfoValue, AuthInfo.class);
        if(authInfo == null) {
            return null;
        }
        return authInfo.getAccessToken();
    }


    @Override
    public OAuth2AccessToken readAccessTokenByUsername(String username) {
        String username2AccessKey = getUsername2AccessKey(username);
        String oAuth2AccessTokenValue = (String) RedisUtil.getObject(username2AccessKey);
        return deserialize(oAuth2AccessTokenValue, OAuth2AccessToken.class);
    }


    @Override
    public OAuth2Authentication readAuthentication(String token) {
        String authInfoKey = getAuthKey(token);
        String authInfoValue = (String) RedisUtil.getObject(authInfoKey);
        AuthInfo authInfo = deserialize(authInfoValue, AuthInfo.class);
        if(authInfo == null) {
            return null;
        }
        return authInfo.buildAuthentication();
    }


    @Override
    public OAuth2Authentication readAuthentication(OAuth2AccessToken token) {
        if(token == null) {
            return null;
        }
        return readAuthentication(token.getValue());
    }


    @Override
    public OAuth2RefreshToken readRefreshToken(String tokenValue) {
        // 资源服务器无需实现
        return null;
    }


    @Override
    public OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token) {
        // 资源服务器无需实现
        return null;
    }


    @Override
    public Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String clientId, String userName) {
        String username2AccessKey = getUsername2AccessKey(userName);
        String oAuth2AccessTokenValue = (String) RedisUtil.getObject(username2AccessKey);
        OAuth2AccessToken accessToken = deserialize(oAuth2AccessTokenValue, OAuth2AccessToken.class);
        if(accessToken == null) {
            return Collections.emptySet();
        }
        List<OAuth2AccessToken> accessTokens = new ArrayList<>(1);
        accessTokens.add(accessToken);
        return Collections.unmodifiableCollection(accessTokens);
    }


    /**
     * 查了源码没有地方有调用到这个方法，所以不实现这个逻辑
     * @param clientId
     * @return
     */
    @Override
    public Collection<OAuth2AccessToken> findTokensByClientId(String clientId) {
        return null;
    }


    @Override
    public void removeAccessToken(OAuth2AccessToken token) {
        OAuth2Authentication authentication = readAuthentication(token);
        if(authentication == null) {
            return;
        }
        RedisUtil.deleteOne(getAuthKey(token.getValue()));
        RedisUtil.deleteOne(getUsername2AccessKey(authentication.getName()));
        RedisUtil.deleteOne(getPermissionKey(token.getValue()));

        UserDetailsInfo userInfo = (UserDetailsInfo) authentication.getPrincipal();
        if(StrUtil.isNotEmpty(userInfo.getOrgCode())) {
            redisTemplate.opsForZSet().remove(getZsetOrgCodeKey(userInfo.getOrgCode()), token.getValue());
        }
        if(StrUtil.isNotEmpty(userInfo.getDistrictCode())) {
            redisTemplate.opsForZSet().remove(getZsetDistrictCodeKey(userInfo.getDistrictCode()), token.getValue());
        }
    }


    @Override
    public void removeAccessTokenAndRefreshToken(AuthInfo authInfo) {
        // 资源服务器无需实现
    }


    @Override
    public void removeRefreshToken(OAuth2RefreshToken token) {
        // 资源服务器无需实现
    }


    @Override
    public void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) {
        // 资源服务器无需实现
    }


    @Override
    public String serialize(Object object) {
        return new String(serializationStrategy.serialize(object));
    }


    @Override
    public <T> T deserialize(String objectJsonStr, Class<T> clazz) {
        if(StrUtil.isEmpty(objectJsonStr)) {
            return null;
        }
        return serializationStrategy.deserialize(objectJsonStr.getBytes(StandardCharsets.UTF_8), clazz);
    }


    @Override
    public AuthInfo readAuthInfo(String accessToken) {
        return deserialize(readAuthInfoValue(accessToken), AuthInfo.class);
    }


    @Override
    public List<String> readPermission(String accessToken) {
        if (StrUtil.isEmpty(accessToken)) {
            return null;
        }
        String permissionKey = getPermissionKey(accessToken);
        return (List<String>) redisTemplate.opsForValue().get(permissionKey);
    }


    @Override
    public String readAuthInfoValue(String accessToken) {
        String authKey = getAuthKey(accessToken);
        return (String) RedisUtil.getObject(authKey);
    }


    @Override
    public long doMaintenance() {
        // 资源服务器无需实现
        return 0L;
    }


    /**
     * ONELINK:UPMS:TOKEN:AUTH:${access_token}:
     * @param token
     * @return
     */
    private String getAuthKey(String token) {
        return ONELINK_UPMS_TOKEN + AUTH + token;
    }


    /**
     * ONELINK:UPMS:TOKEN:USERNAME_2_ACCESS:${username}:
     * @param username
     * @return
     */
    private String getUsername2AccessKey(String username) {
        return ONELINK_UPMS_TOKEN + USERNAME_2_ACCESS + username;
    }


    /**
     * ONELINK:UPMS:TOKEN:PERMISSION:${access_token}
     * @param token
     * @return
     */
    private String getPermissionKey(String token) {
        return ONELINK_UPMS_TOKEN + PERMISSION + token;
    }


    /**
     * ONELINK:UPMS:TOKEN:ZSET:${orgCode}:
     * @param orgCode
     * @return
     */
    private String getZsetOrgCodeKey(String orgCode) {
        return ONELINK_UPMS_TOKEN + ZSET + orgCode;
    }


    /**
     * ONELINK:UPMS:TOKEN:ZSET:${districtCode}:
     * @param districtCode
     * @return
     */
    private String getZsetDistrictCodeKey(String districtCode) {
        return ONELINK_UPMS_TOKEN + ZSET + districtCode;
    }


    /**
     * ONELINK:UPMS:TOKEN:REFRESH:{refresh_token}:
     * @param refreshToken
     * @return
     */
    private String getRefreshKey(String refreshToken) {
        return ONELINK_UPMS_TOKEN + REFRESH + refreshToken;
    }


    /**
     * ONELINK:UPMS:TOKEN:REFRESH_2_AUTH:{refresh_token}:
     * @param refreshToken
     * @return
     */
    private String getRefresh2AuthKey(String refreshToken) {
        return ONELINK_UPMS_TOKEN + REFRESH_2_AUTH + refreshToken;
    }
}
